﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.UI;

namespace HandsonSoft.Web.WebFormsModelBinder
{
    public static class ModelBinder
    {
        private const string CONTROL_NOT_FOUND_ERROR = "Control name '{0}' not found.";
        private const string VALUE_SOURCE_NOT_FOUND_ERROR = "Value source '{0}' on property '{1}' not found.";
        private const string FIND_METHOD_NOT_FOUND_ERROR = "Find method '{0}' on property '{1}' not found.";
        private const string ADD_METHOD_NOT_FOUND_ERROR = "Add method '{0}' on property '{1}' not found.";
        private const string COLLECTION_NOT_FOUND_ERROR = "Items collection '{0}' on property '{1}' not found.";
        private const string SELECTED_NOT_FOUND_ERROR = "SelectedProperty '{0}' on property '{1}' not found.";
        private const string SET_VALUE_ERROR = "Error on Property: '{0}' - {1}";

        public static T BindModel<T>(Control page) where T : class
        {
            var type = typeof(T);
            var properties = type.GetProperties();
            var instance = Activator.CreateInstance(type);
            var instanceType = instance.GetType();
            var raiseError = RaiseError(type);

            BindModel(instance, properties, page, raiseError, instanceType);

            return (T)instance;
        }

        private static void BindModel(object instance, IEnumerable<PropertyInfo> properties, Control page, bool raiseError, Type instanceType)
        {
            foreach (var propertyInfo in properties)
            {
                var isNotMapped = IsNotMapped(propertyInfo);

                if (isNotMapped)
                    continue;

                var isComplexType = IsComplexType(propertyInfo);

                if (isComplexType)
                {
                    var complexTypeInstance = Activator.CreateInstance(propertyInfo.PropertyType);
                    var complexTypeInstanceType = complexTypeInstance.GetType();
                    var complexTypeProperties = complexTypeInstanceType.GetProperties(BindingFlags.Instance | BindingFlags.Public);

                    BindModel(complexTypeInstance, complexTypeProperties, page, raiseError, complexTypeInstanceType);

                    instanceType.InvokeMember(
                        propertyInfo.Name,
                        BindingFlags.SetProperty,
                        null,
                        instance,
                        new[] { complexTypeInstance }
                        );

                    continue;
                }

                var mc = GetControlId(propertyInfo);
                var control = page.FindControl(mc.ControlId ?? propertyInfo.Name);

                if (control == null && raiseError)
                    throw new BindingException(string.Format(CONTROL_NOT_FOUND_ERROR, propertyInfo.Name));

                if (control == null)
                    continue;

                var t = control.GetType();
                var valueSource = GetValueSource(propertyInfo);
                var useTypeConverter = UseTypeConverter(propertyInfo);
                var ic = IsCollection(propertyInfo);

                if (ic)
                {
                    BindCollection(instance, propertyInfo, control, instanceType, t, useTypeConverter, valueSource);
                    continue;
                }

                var pi = t.GetProperty(valueSource.Name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);

                if (pi == null && raiseError)
                    throw new BindingException(string.Format(VALUE_SOURCE_NOT_FOUND_ERROR, valueSource.Name, propertyInfo.Name));

                if (pi == null)
                    continue;

                var value = pi.GetValue(control, null);
                var instancePropertyInfo = instanceType.GetProperty(propertyInfo.Name);

                if (!useTypeConverter)
                {
                    try
                    {
                        instancePropertyInfo.SetValue(instance, value, null);
                    }
                    catch (Exception e)
                    {
                        throw new BindingException(string.Format(SET_VALUE_ERROR, string.Format("{0}.{1}", propertyInfo.DeclaringType.FullName, propertyInfo.Name), e.Message), e);
                    }

                    continue;
                }

                var pd = TypeDescriptor.GetProperties(instance).Find(propertyInfo.Name, true);
                var tc = pd.Converter;

                if (tc == null)
                    continue;

                try
                {
                    instancePropertyInfo.SetValue(instance, tc.ConvertFrom(value), null);
                }
                catch (Exception e)
                {
                    throw new BindingException(string.Format(SET_VALUE_ERROR, string.Format("{0}.{1}", propertyInfo.DeclaringType.FullName, propertyInfo.Name), e.Message), e);
                }
            }
        }

        public static void BindControl(object obj, Control page)
        {
            if (obj == null)
                return;

            var properties = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            var instance = obj;
            var instanceType = instance.GetType();
            var raiseError = RaiseError(instanceType);

            BindControl(instance, instanceType, properties, page, raiseError);
        }

        private static void BindControl(object instance, Type instanceType, IEnumerable<PropertyInfo> properties, Control page, bool raiseError)
        {
            foreach (var propertyInfo in properties)
            {
                var isNotMapped = IsNotMapped(propertyInfo);

                if (isNotMapped)
                    continue;

                var isComplexType = IsComplexType(propertyInfo);

                if (isComplexType)
                {
                    var complexTypeInstance = instanceType.InvokeMember(propertyInfo.Name, BindingFlags.GetProperty, null, instance, null);

                    if (complexTypeInstance == null)
                        continue;

                    var complexTypeInstanceType = complexTypeInstance.GetType();
                    var complexTypeProperties = complexTypeInstanceType.GetProperties(BindingFlags.Instance | BindingFlags.Public);

                    BindControl(complexTypeInstance, complexTypeInstanceType, complexTypeProperties, page, raiseError);
                    continue;
                }

                var mc = GetControlId(propertyInfo);
                var control = page.FindControl(mc.ControlId ?? propertyInfo.Name);

                if (control == null && raiseError)
                    throw new BindingException(string.Format(CONTROL_NOT_FOUND_ERROR, propertyInfo.Name));

                if (control == null)
                    continue;

                var t = control.GetType();
                var valueSource = GetValueSource(propertyInfo);
                var useTypeConverter = UseTypeConverter(propertyInfo);
                var ic = IsCollection(propertyInfo);

                if (ic)
                {
                    BindCollectionToControl(instance, propertyInfo, control, instanceType, t, useTypeConverter);
                    continue;
                }

                var pi = t.GetProperty(valueSource.Name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);

                if (pi == null && raiseError)
                    throw new BindingException(string.Format(VALUE_SOURCE_NOT_FOUND_ERROR, valueSource.Name, propertyInfo.Name));

                if (pi == null)
                    continue;

                var instancePropertyInfo = instanceType.GetProperty(propertyInfo.Name);
                var value = instancePropertyInfo.GetValue(instance, null);

                if (!useTypeConverter)
                {
                    try
                    {
                        pi.SetValue(control, value, null);
                    }
                    catch (Exception e)
                    {
                        throw new BindingException(string.Format(SET_VALUE_ERROR, string.Format("{0}.{1}", propertyInfo.DeclaringType.FullName, propertyInfo.Name), e.Message), e);
                    }

                    continue;
                }

                var pd = TypeDescriptor.GetProperties(control).Find(valueSource.Name, true);

                try
                {
                    pi.SetValue(control, Convert.ChangeType(value, pd.PropertyType), null);
                }
                catch (Exception e)
                {
                    throw new BindingException(string.Format(SET_VALUE_ERROR, string.Format("{0}.{1}", propertyInfo.DeclaringType.FullName, propertyInfo.Name), e.Message), e);
                }
            }
        }

        private static void BindCollection(
            object instance,
            PropertyInfo propertyInfo,
            Control control,
            Type instanceType,
            Type t,
            bool useTypeConverter,
            ValueSource valueSource)
        {
            var vl = GetValueList(propertyInfo);
            var propertyInstance = Activator.CreateInstance(propertyInfo.PropertyType);
            var add = propertyInstance.GetType().GetMethod(vl.AddMethod);
            var raiseError = RaiseError(instanceType);

            if (add == null && raiseError)
                throw new BindingException(string.Format(ADD_METHOD_NOT_FOUND_ERROR, vl.AddMethod, propertyInfo.Name));

            if (add == null)
                return;

            var p = add.GetParameters();

            if (p.Count() != 1)
                return;

            var listObject = t.InvokeMember(vl.CollectionProperty, BindingFlags.GetProperty, null, control, null);

            if (listObject == null && raiseError)
                throw new BindingException(string.Format(COLLECTION_NOT_FOUND_ERROR, vl.CollectionProperty, propertyInfo.Name));

            if (listObject == null)
                return;

            foreach (var i in (IEnumerable)listObject)
            {
                var itemType = i.GetType();
                var selected = itemType.GetProperty(vl.SelectedProperty);

                if (selected == null && raiseError)
                    throw new BindingException(string.Format(SELECTED_NOT_FOUND_ERROR, vl.SelectedProperty, propertyInfo.Name));

                if (selected == null)
                    continue;

                var isSelected = selected.GetValue(i, null);

                if (isSelected == null || (bool)isSelected == false)
                    continue;

                var vp = itemType.GetProperty(valueSource.Name);

                if (vp == null && raiseError)
                    throw new BindingException(string.Format(VALUE_SOURCE_NOT_FOUND_ERROR, valueSource.Name, propertyInfo.Name));

                if (vp == null)
                    continue;

                var value = vp.GetValue(i, null);

                add.Invoke(propertyInstance, new[] { useTypeConverter ? Convert.ChangeType(value, p[0].ParameterType) : value });
            }

            instanceType.InvokeMember(
                propertyInfo.Name,
                BindingFlags.SetProperty,
                null,
                instance,
                new[] { propertyInstance }
                );
        }

        private static void BindCollectionToControl(
            object instance,
            PropertyInfo propertyInfo,
            Control control,
            Type instanceType,
            Type t,
            bool useTypeConverter)
        {
            var vl = GetValueList(propertyInfo);
            var listValue = instanceType.GetProperty(propertyInfo.Name).GetValue(instance, null);
            var raiseError = RaiseError(instanceType);

            if (listValue == null)
                return;

            var cpi = t.GetProperty(vl.CollectionProperty);

            if (cpi == null)
                return;

            var listObject = t.InvokeMember(vl.CollectionProperty, BindingFlags.GetProperty, null, control, null);

            if (listObject == null && raiseError)
                throw new BindingException(string.Format(COLLECTION_NOT_FOUND_ERROR, vl.CollectionProperty, propertyInfo.Name));

            if (listObject == null)
                return;

            var findMethod = vl.FindMethodInParent ? t.GetMethod(vl.FindMethod) : cpi.PropertyType.GetMethod(vl.FindMethod);

            if (findMethod == null && raiseError)
                throw new BindingException(string.Format(FIND_METHOD_NOT_FOUND_ERROR, vl.FindMethod, propertyInfo.Name));

            if (findMethod == null)
                return;

            var p = findMethod.GetParameters();

            if (p.Count() != 1)
                return;

            foreach (var i in (IEnumerable)listValue)
            {
                var target = vl.FindMethodInParent ? control : listObject;
                var item = findMethod.Invoke(target, new[] { useTypeConverter ? Convert.ChangeType(i, p[0].ParameterType) : i });

                if (item == null)
                    continue;

                var itemType = item.GetType();
                var selected = itemType.GetProperty(vl.SelectedProperty);

                if (selected == null && raiseError)
                    throw new BindingException(string.Format(SELECTED_NOT_FOUND_ERROR, vl.SelectedProperty, propertyInfo.Name));

                if (selected == null)
                    continue;

                selected.SetValue(item, true, null);
            }
        }

        private static ValueSource GetValueSource(PropertyInfo pi)
        {
            var cacheKey = string.Format("ValueSource:{0}", string.Format("{0}.{1}", pi.DeclaringType.FullName, pi.Name));
            var cache = HttpRuntime.Cache;
            var attrib = cache.Get(cacheKey);

            if (attrib == null)
            {
                attrib = pi.GetCustomAttributes(typeof(ValueSource), false);
                cache.Insert(cacheKey, attrib);
            }

            return ((object[])attrib).Count() == 0 ? new ValueSource() : ((ValueSource)((object[])attrib)[0]);
        }

        private static ValueList GetValueList(PropertyInfo pi)
        {
            var cacheKey = string.Format("ValueList:{0}", string.Format("{0}.{1}", pi.DeclaringType.FullName, pi.Name));
            var cache = HttpRuntime.Cache;
            var attrib = cache.Get(cacheKey);

            if (attrib == null)
            {
                attrib = pi.GetCustomAttributes(typeof(ValueList), false);
                cache.Insert(cacheKey, attrib);
            }

            return ((object[])attrib).Count() == 0 ? new ValueList() : (ValueList)((object[])attrib)[0];
        }

        private static bool UseTypeConverter(PropertyInfo pi)
        {
            var cacheKey = string.Format("TypeConvert:{0}", string.Format("{0}.{1}", pi.DeclaringType.FullName, pi.Name));
            var cache = HttpRuntime.Cache;
            var attrib = cache.Get(cacheKey);

            if (attrib == null)
            {
                attrib = pi.GetCustomAttributes(typeof(TypeConvert), false);
                cache.Insert(cacheKey, attrib);
            }

            return ((object[])attrib).Count() == 0 ? false : true;
        }

        private static bool IsNotMapped(PropertyInfo pi)
        {
            var cacheKey = string.Format("NotMapped:{0}", string.Format("{0}.{1}", pi.DeclaringType.FullName, pi.Name));
            var cache = HttpRuntime.Cache;
            var attrib = cache.Get(cacheKey);

            if (attrib == null)
            {
                attrib = pi.GetCustomAttributes(typeof(NotMapped), false);
                cache.Insert(cacheKey, attrib);
            }

            return ((object[])attrib).Count() == 0 ? false : true;
        }

        private static bool IsCollection(PropertyInfo pi)
        {
            var cacheKey = string.Format("IsCollection:{0}", string.Format("{0}.{1}", pi.DeclaringType.FullName, pi.Name));
            var cache = HttpRuntime.Cache;
            var obj = cache.Get(cacheKey);

            if (obj == null)
            {
                obj = pi.PropertyType.GetInterface(typeof(ICollection).Name) != null;
                cache.Insert(cacheKey, obj);
            }

            return (bool)obj;
        }

        private static bool RaiseError(Type type)
        {
            var cacheKey = string.Format("RaiseError:{0}", type.FullName);
            var cache = HttpRuntime.Cache;
            var attrib = cache.Get(cacheKey);

            if (attrib == null)
            {
                attrib = type.GetCustomAttributes(typeof(RaiseBindingError), false);
                cache.Insert(cacheKey, attrib);
            }

            return ((object[])attrib).Count() == 0 ? false : true;
        }

        private static MapToControl GetControlId(PropertyInfo pi)
        {
            var cacheKey = string.Format("MapToControl:{0}", string.Format("{0}.{1}", pi.DeclaringType.FullName, pi.Name));
            var cache = HttpRuntime.Cache;
            var attrib = cache.Get(cacheKey);

            if (attrib == null)
            {
                attrib = pi.GetCustomAttributes(typeof(MapToControl), false);
                cache.Insert(cacheKey, attrib);
            }

            return ((object[])attrib).Count() == 0 ? new MapToControl() : (MapToControl)((object[])attrib)[0];
        }

        private static bool IsComplexType(PropertyInfo pi)
        {
            var cacheKey = string.Format("Include:{0}", string.Format("{0}.{1}", pi.DeclaringType.FullName, pi.Name));
            var cache = HttpRuntime.Cache;
            var attrib = cache.Get(cacheKey);

            if (attrib == null)
            {
                attrib = pi.GetCustomAttributes(typeof(Include), false);
                cache.Insert(cacheKey, attrib);
            }

            return ((object[])attrib).Count() == 0 ? false : true;
        }
    }
}
